//
// Copyright 2017 Google Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

// stylelint-disable selector-class-pattern --
// Selector '.mdc-*' should only be used in this project.

@use 'sass:math';
@use 'sass:list';
@use 'sass:map';
@use 'sass:meta';
@use 'sass:string';
@use '@material/feature-targeting/feature-targeting';
@use '@material/theme/custom-properties';
@use '@material/theme/keys';
@use '@material/theme/theme';

/// @deprecated Avoid calling this function directly. Instead, configure the
/// `$styles-<style>` variable Maps.
@function set-styles_($base-styles, $scale-styles, $override-styles) {
  $options: (
    custom-property-prefix: typography,
  );

  $base-styles: keys.set-values($base-styles, $options: $options);

  @each $style, $style-props in $scale-styles {
    @each $base-key in map.keys($base-styles) {
      // Ignore the return result, it's not needed
      $unused: keys.add-link(keys.combine($style, $base-key), $base-key);
    }

    // Merge base properties for all styles.
    $style-props: map.merge($base-styles, $style-props);

    // Merge overrides onto each style.
    $style-props: map.merge($style-props, map.get($override-styles, $style));

    // Register keys for this style
    @each $property, $value in $style-props {
      $unused: keys.set-value(
        keys.combine($style, $property),
        $value,
        $options: $options
      );
    }

    // Override original styles with new styles.
    $scale-styles: map.merge($scale-styles, (#{$style}: $style-props));
  }

  @return $scale-styles;
}

@function get-letter-spacing_($tracking, $font-size) {
  @return math.div($tracking, $font-size * 16) * 1em;
}

@function px-to-rem($px) {
  @if custom-properties.is-custom-prop($px) {
    @return custom-properties.set-fallback(
      $px,
      _px-to-rem(custom-properties.get-fallback($px))
    );
  }
  @return _px-to-rem($px);
}

@function _px-to-rem($px) {
  @if $px == null {
    @return null;
  }
  @return math.div($px, 16px) * 1rem;
}

$font-family: string.unquote('Roboto, sans-serif') !default;

// Override styles
$styles-headline1: () !default;
$styles-headline2: () !default;
$styles-headline3: () !default;
$styles-headline4: () !default;
$styles-headline5: () !default;
$styles-headline6: () !default;
$styles-subtitle1: () !default;
$styles-subtitle2: () !default;
$styles-body1: () !default;
$styles-body2: () !default;
$styles-caption: () !default;
$styles-button: () !default;
$styles-overline: () !default;

/// @deprecated Do not override this variable. Use the $styles-<style> override
/// Map variables instead, or $font-family to set the base font family.
$base: (
  font-family: $font-family,
) !default;

$font-weight-values: (
  thin: 100,
  light: 300,
  regular: 400,
  medium: 500,
  bold: 700,
  black: 900,
) !default;

/// @deprecated Do not override this variable. Use the $styles-<style> override
/// Map variables instead.
$styles: set-styles_(
  $base,
  (
    headline1: (
      font-size: px-to-rem(96px),
      line-height: px-to-rem(96px),
      font-weight: map.get($font-weight-values, light),
      letter-spacing: get-letter-spacing_(-1.5, 6),
      text-decoration: inherit,
      text-transform: inherit,
    ),
    headline2: (
      font-size: px-to-rem(60px),
      line-height: px-to-rem(60px),
      font-weight: map.get($font-weight-values, light),
      letter-spacing: get-letter-spacing_(-0.5, 3.75),
      text-decoration: inherit,
      text-transform: inherit,
    ),
    headline3: (
      font-size: px-to-rem(48px),
      line-height: px-to-rem(50px),
      font-weight: map.get($font-weight-values, regular),
      letter-spacing: normal,
      text-decoration: inherit,
      text-transform: inherit,
    ),
    headline4: (
      font-size: px-to-rem(34px),
      line-height: px-to-rem(40px),
      font-weight: map.get($font-weight-values, regular),
      letter-spacing: get-letter-spacing_(0.25, 2.125),
      text-decoration: inherit,
      text-transform: inherit,
    ),
    headline5: (
      font-size: px-to-rem(24px),
      line-height: px-to-rem(32px),
      font-weight: map.get($font-weight-values, regular),
      letter-spacing: normal,
      text-decoration: inherit,
      text-transform: inherit,
    ),
    headline6: (
      font-size: px-to-rem(20px),
      line-height: px-to-rem(32px),
      font-weight: map.get($font-weight-values, medium),
      letter-spacing: get-letter-spacing_(0.25, 1.25),
      text-decoration: inherit,
      text-transform: inherit,
    ),
    subtitle1: (
      font-size: px-to-rem(16px),
      line-height: px-to-rem(28px),
      font-weight: map.get($font-weight-values, regular),
      letter-spacing: get-letter-spacing_(0.15, 1),
      text-decoration: inherit,
      text-transform: inherit,
    ),
    subtitle2: (
      font-size: px-to-rem(14px),
      line-height: px-to-rem(22px),
      font-weight: map.get($font-weight-values, medium),
      letter-spacing: get-letter-spacing_(0.1, 0.875),
      text-decoration: inherit,
      text-transform: inherit,
    ),
    body1: (
      font-size: px-to-rem(16px),
      line-height: px-to-rem(24px),
      font-weight: map.get($font-weight-values, regular),
      letter-spacing: get-letter-spacing_(0.5, 1),
      text-decoration: inherit,
      text-transform: inherit,
    ),
    body2: (
      font-size: px-to-rem(14px),
      line-height: px-to-rem(20px),
      font-weight: map.get($font-weight-values, regular),
      letter-spacing: get-letter-spacing_(0.25, 0.875),
      text-decoration: inherit,
      text-transform: inherit,
    ),
    caption: (
      font-size: px-to-rem(12px),
      line-height: px-to-rem(20px),
      font-weight: map.get($font-weight-values, regular),
      letter-spacing: get-letter-spacing_(0.4, 0.75),
      text-decoration: inherit,
      text-transform: inherit,
    ),
    button: (
      font-size: px-to-rem(14px),
      line-height: px-to-rem(36px),
      font-weight: map.get($font-weight-values, medium),
      letter-spacing: get-letter-spacing_(1.25, 0.875),
      text-decoration: none,
      text-transform: uppercase,
    ),
    overline: (
      font-size: px-to-rem(12px),
      line-height: px-to-rem(32px),
      font-weight: map.get($font-weight-values, medium),
      letter-spacing: get-letter-spacing_(2, 0.75),
      text-decoration: none,
      text-transform: uppercase,
    ),
  ),
  (
    headline1: $styles-headline1,
    headline2: $styles-headline2,
    headline3: $styles-headline3,
    headline4: $styles-headline4,
    headline5: $styles-headline5,
    headline6: $styles-headline6,
    subtitle1: $styles-subtitle1,
    subtitle2: $styles-subtitle2,
    body1: $styles-body1,
    body2: $styles-body2,
    caption: $styles-caption,
    button: $styles-button,
    overline: $styles-overline,
  )
) !default;

// A copy of the styles Map that is used to detect compile-time changes for
// Angular support.
$_styles-copy: $styles;

@function is-typography-style($style) {
  @return map.has-key($styles, $style);
}

@function get-typography-styles() {
  @return map.keys($styles);
}

@mixin core-styles($query: feature-targeting.all()) {
  .mdc-typography {
    @include base($query: $query);
  }

  @each $style in get-typography-styles() {
    .mdc-typography--#{$style} {
      @include typography($style, $query: $query);
    }
  }
}

@mixin base($query: feature-targeting.all()) {
  $feat-typography: feature-targeting.create-target($query, typography);

  @include smooth-font($query: $query);
  @include feature-targeting.targets($feat-typography) {
    @include theme.property(font-family, font-family);
  }
}

@mixin typography($style, $query: feature-targeting.all(), $exclude-props: ()) {
  $feat-typography: feature-targeting.create-target($query, typography);

  @if not is-typography-style($style) {
    @error "Invalid style specified! #{$style} doesn't exist. Choose one of #{get-typography-styles()}";
  }

  @include smooth-font($query: $query);
  @include feature-targeting.targets($feat-typography) {
    @each $key in keys.get-keys($style) {
      // <style>-<property>: headline1-font-size
      // Slice the string past the first key separator to retrieve the
      // property name
      $property: string.slice($key, string.index($key, '-') + 1);
      @if list.index($exclude-props, $property) == null {
        $current-global-value: map.get($styles, $style, $property);
        $configured-global-value: map.get($_styles-copy, $style, $property);
        @if $current-global-value != $configured-global-value {
          // A compile time change was made to $mdc-typography-styles. To
          // support Angular, use this value instead of the key's value.
          @if $current-global-value {
            // Only emit if the overridden value exists
            $custom-prop: keys.create-custom-property($key);
            $custom-prop: custom-properties.set-fallback(
              $custom-prop,
              $current-global-value
            );
            @include theme.property($property, $custom-prop);
          }
        } @else {
          // Otherwise, use the key, which may be different from the original
          // configured global value.
          @include theme.property($property, $key);
        }
      }
    }
  }
}

/// Applies antialiasing via font-smoothing to text.
@mixin smooth-font($query: feature-targeting.all()) {
  $feat-typography: feature-targeting.create-target($query, typography);

  @include feature-targeting.targets($feat-typography) {
    -moz-osx-font-smoothing: grayscale;
    -webkit-font-smoothing: antialiased;
  }
}

// Element must be `display: block` or `display: inline-block` for this to work.
@mixin overflow-ellipsis($query: feature-targeting.all()) {
  $feat-structure: feature-targeting.create-target($query, structure);

  @include feature-targeting.targets($feat-structure) {
    text-overflow: ellipsis;
    white-space: nowrap;
    overflow: hidden;
  }
}

/// Sets a container's baseline that text content will align to.
///
/// If the `$display` is set to a flexbox display, only `$top` baseline may be
/// set. A separate element must be added as a child of the container with a
/// `$bottom` baseline.
///
/// @param {Number} $top - the distance from the top of the container to the
///     text's baseline.
/// @param {Number} $bottom - the distance from the text's baseline to the
///     bottom of the container.
/// @param {String} $display - the display type of the container. May be `flex`,
///     `inline-flex`, `block`, or `inline-block`.
@mixin baseline(
  $top: 0,
  $bottom: 0,
  $display: block,
  $query: feature-targeting.all()
) {
  $validDisplayTypes: (flex, inline-flex, block, inline-block);

  @if list.index($validDisplayTypes, $display) == null {
    @error "mdc-typography: invalid display specified! #{$display} must be one of #{$validDisplayTypes}";
  }

  $isFlexbox: $display == 'flex' or $display == 'inline-flex';
  $feat-structure: feature-targeting.create-target($query, structure);

  @include feature-targeting.targets($feat-structure) {
    display: $display;

    @if $isFlexbox {
      align-items: baseline;
    }
  }

  @if $top > 0 {
    @include baseline-top($top, $query: $query);
  }

  @if $bottom > 0 {
    @if $isFlexbox {
      @error "mdc-typography: invalid baseline with display type. #{$display} cannot specifiy $bottom. Add a separate child element with its own $bottom.";
    }

    @include baseline-bottom($bottom, $query: $query);
  }
}

/// Sets the baseline of flow text content.
///
/// Separate `$top` and `$bottom` baselines may be specified. You should ensure
/// that the `$top` baseline matches the previous text content's $bottom
/// baseline to ensure text is positioned appropriately.
///
/// See go/css-baseline for reference on how this mixin works.
///
/// This is intended for text flow content only (e.g. `<h1>`, `<p>`, `<span>`,
/// or `<div>` with only text content). Use `baseline()` to set the baseline of
/// containers that are flexbox or have non-flow content children.
///
/// @param {Number} $top - the distance from the top of the container to the
///     text's baseline.
/// @param {Number} $bottom - the distance from the text's baseline to the
///     bottom of the container.
/// @param {Boolean} $lineHeight - the line-height to use for the text. This
///     is the distance between baselines of multiple lines of text.
/// @param {String} $display - the display type of the container. May be `block`
///     or `inline-block`.
@mixin text-baseline(
  $top: 0,
  $bottom: 0,
  $display: block,
  $lineHeight: normal,
  $query: feature-targeting.all()
) {
  $validDisplayTypes: (block, inline-block);

  @if list.index($validDisplayTypes, $display) == null {
    @error "mdc-typography: invalid display specified! #{$display} must be one of #{$validDisplayTypes}";
  }

  $feat-structure: feature-targeting.create-target($query, structure);

  @include baseline(
    $display: $display,
    $top: $top,
    $bottom: $bottom,
    $query: $query
  );
  @include feature-targeting.targets($feat-structure) {
    @if $top > 0 {
      margin-top: 0;
      /* @alternate */
      line-height: #{$lineHeight};
    }

    @if $bottom > 0 {
      margin-bottom: -1 * $bottom;
    }
  }
}

/// Creates a baseline strut from the top of a container. This mixin is for
/// advanced users, prefer `baseline()`.
///
/// @param {Number} $distance - The distance from the top of the container to
///     the text's baseline.
@mixin baseline-top($distance, $query: feature-targeting.all()) {
  $feat-structure: feature-targeting.create-target($query, structure);

  &::before {
    @include feature-targeting.targets($feat-structure) {
      @include baseline-strut_($distance);

      vertical-align: 0;
    }
  }
}

/// Creates a baseline strut from the baseline to the bottom of a container.
/// This mixin is for advanced users, prefer `baseline()`.
///
/// @param {Number} $distance - The distance from the text's baseline to the
///     bottom of the container.
@mixin baseline-bottom($distance, $query: feature-targeting.all()) {
  $feat-structure: feature-targeting.create-target($query, structure);

  &::after {
    @include feature-targeting.targets($feat-structure) {
      @include baseline-strut_($distance);

      vertical-align: -1 * $distance;
    }
  }
}

/// Adds an invisible, zero-width prefix to a container's text.
/// This ensures that the baseline is always where the text would be, instead
/// of defaulting to the container bottom when text is empty. Do not use this
/// mixin if the `baseline` mixin is already applied.
@mixin zero-width-prefix($query: feature-targeting.all()) {
  $feat-structure: feature-targeting.create-target($query, structure);

  &::before {
    @include feature-targeting.targets($feat-structure) {
      content: '\200b';
    }
  }
}

@mixin baseline-strut_($distance) {
  display: inline-block;
  width: 0;
  height: $distance;
  content: '';
}

@function get-font($typography) {
  @return map.get($styles, $typography, font-family);
}

@function get-line-height($typography) {
  @return map.get($styles, $typography, line-height);
}

@function get-size($typography) {
  @return map.get($styles, $typography, font-size);
}

@function get-weight($typography) {
  @return map.get($styles, $typography, font-weight);
}

@function get-tracking($typography) {
  @return map.get($styles, $typography, letter-spacing);
}

$_typography-theme: (
  font: null,
  line-height: null,
  size: null,
  weight: null,
  tracking: null,
);

@mixin theme-styles($theme) {
  @include theme.validate-theme-styles($_typography-theme, $theme);

  @include theme.property(font-family, map.get($theme, font));
  @include theme.property(line-height, map.get($theme, line-height));
  @include theme.property(font-size, map.get($theme, size));
  @include theme.property(font-weight, map.get($theme, weight));
  @include theme.property(letter-spacing, map.get($theme, tracking));
}

/// Resolves a theme's typography tokens for the given prefix.
///
/// @example - scss
///   // $theme has the following tokens:
///   // - label-text-font
///   // - label-text-line-height
///   // - label-text-size
///   // - label-text-tracking
///   // - label-text-weight
///   $theme: resolve-theme($theme, map.get($resolvers, typography), label-text);
///
/// @param {Map} $theme - The theme to resolve tokens for.
/// @param {Function} $resolver - The typography resolver to use.
/// @param {String...} $token-prefixes - The prefix(es) of a typography token
///     set.
/// @return {Map} The theme with resolved typography tokens.
@function resolve-theme($theme, $resolver, $token-prefixes...) {
  @if $resolver == null {
    @return $theme;
  }

  @each $token-prefix in $token-prefixes {
    $typography-theme: meta.call(
      $resolver,
      $font: map.get($theme, '#{$token-prefix}-font'),
      $line-height: map.get($theme, '#{$token-prefix}-line-height'),
      $size: map.get($theme, '#{$token-prefix}-size'),
      $tracking: map.get($theme, '#{$token-prefix}-tracking'),
      $weight: map.get($theme, '#{$token-prefix}-weight')
    );

    $theme: map.merge(
      $theme,
      (
        '#{$token-prefix}-font': map.get($typography-theme, font),
        '#{$token-prefix}-line-height': map.get($typography-theme, line-height),
        '#{$token-prefix}-size': map.get($typography-theme, size),
        '#{$token-prefix}-tracking': map.get($typography-theme, tracking),
        '#{$token-prefix}-weight': map.get($typography-theme, weight),
      )
    );
  }

  @return $theme;
}
